创建对象的方式通常是创建一个 Object 实例,然后再给它添加属性和方法:
let person = new Object();
person.name = "Nicholas";
person.age = 29;
person.job = "Software Engineer";
person.sayName = function() {
console.log(this.name);
};
也可以通过字面量的形式创建
let person = {
name: "Nicholas",
age: 29,
job: "Software Engineer",
sayName() {
console.log(this.name);
}
};
数据属性有 4 个特性描述它们的行为
想要修改属性的默认特性,就必须使用 Object.defineProperty() 方法。该方法接收 3 个参数:要添加属性的对象、属性的名称和描述符对象。描述符对象上的属性可以包含:configurable、enumerable、writable 和 value。
let person = {};
Object.defineProperty(person, "name", {
writable: false,
value: "Nicholas"
});
console.log(person.name); // "Nicholas"
person.name = "Greg";
console.log(person.name); // "Nicholas"
通过上方代码创建的 person 对象,其 name 属性是不可修改的。在调用Object.defineProperty() 时,configurable、enumerable 和 writable 的值如果不指定,则都默认为 false。
访问器属性也有 4 个特性描述它们的行为
// 定义一个对象,包含伪私有成员year_和公共成员edition
let book = {
year_: 2017,
edition: 1
};
// 为对象定义一个访问器
Object.defineProperty(book, "year", {
get() {
return this.year_;
},
set(newValue) {
if (newValue > 2017) {
this.year_ = newValue;
this.edition += newValue - 2017;
}
}
});
// 通过访问器访问对线的属性
book.year = 2018;
console.log(book.edition); // 2
获取函数和设置函数不一定都要定义,只定义获取函数意味着属性是只读的,尝试修改属性会被忽略
JS 提供了 Object.defineProperties() 方法,用于一次定义多个属性
let book = {};
Object.defineProperties(book, {
year_: {
value: 2017
},
edition: {
value: 1
},
year: {
get() {
return this.year_;
},
set(newValue) {
if (newValue > 2017) {
this.year_ = newValue;
this.edition += newValue - 2017;
}
}
}
});
使用 Object.getOwnPropertyDescriptor() 方法可以取得指定属性的属性描述符,返回值是一个对象,对于访问器属性包含 configurable、enumerable、get 和 set 属性,对于数据属性包含 configurable、enumerable、writable 和 value 属性。例如对于上面代码中定义的 book 对象
let descriptor = Object.getOwnPropertyDescriptor(book, "year_");
console.log(descriptor.value); // 2017
console.log(descriptor.configurable); // false
console.log(typeof descriptor.get); // "undefined"
let descriptor = Object.getOwnPropertyDescriptor(book, "year");
console.log(descriptor.value); // undefined
console.log(descriptor.enumerable); // false
console.log(typeof descriptor.get); // "function"
通过 Object.getOwnPropertyDescriptors() 方法可以一次性取得所有属性的属性描述符
ES6 为合并对象提供了 Object.assign() 方法,这个方法接收一个目标对象和一个或多个源对象作为参数,然后将每个源对象中可枚举( Object.propertyIsEnumerable() 返回 true)和自有(Object.hasOwnProperty() 返回 true)属性复制到目标对象。
let dest, src, result;
dest = {};
src = { id: 'src' };
result = Object.assign(dest, src);
console.log(dest === result); // true
console.log(dest !== src); // true
console.log(result); // { id: src }
console.log(dest); // { id: src }
// 多个源对象
dest = {};
result = Object.assign(dest, { a: 'foo' }, { b: 'bar' });
console.log(result); // { a: foo, b: bar }
// getter 和 setter 方法
dest = {
set a(val) {
console.log(`Invoked dest setter with param ${val}`);
}
};
src = {
get a() {
console.log('Invoked src getter');
return 'foo';
}
};
Object.assign(dest, src);
console.log(dest); // { set a(val) {...} }
// 可以从控制台中看到输出的对象,其中包含 set a: f a(val) 方法
Object.assign() 实际上对每个源对象执行的是浅拷贝。如果多个源对象都有相同的属性,则使用最后一个复制的值。不能在两个对象间转移 getter() 方法和 setter() 方法。如果赋值期间出错,则操作会中止并退出,同时抛出错误,但不会回滚到之前的对象状态。
let dest, src, result;
dest = {};
src = {
a: 'foo',
get b() {
// Object.assign()在调用这个获取函数时会抛出错误
throw new Error();
},
c: 'bar'
};
try {
Object.assign(dest, src);
} catch(e) {}
console.log(dest); // { a: foo }
对象间使用 Object.is() 方法进行相等判断,需要接收两个参数
console.log(Object.is(true, 1)); // false
console.log(Object.is({}, {})); // false
console.log(Object.is("2", 2)); // false
// 正确的 0、-0、+0 相等/不等判定
console.log(Object.is(+0, -0)); // false
console.log(Object.is(+0, 0)); // true
console.log(Object.is(-0, 0)); // false
// 正确的 NaN 相等判定
console.log(Object.is(NaN, NaN)); // true
几个好用的语法糖
let name = 'Matt';
// 属性名与变量名相同,可简写
let person = {
name
};
console.log(person); // { name: 'Matt' }
// 对象通过字面量初始化时,可使用中括号语法声明计算属性
// 中括号中的内容被当做JS表达式进行求值后再显示
const nameKey = 'name';
const ageKey = 'age';
const jobKey = 'job';
let person = {
[nameKey]: 'Matt',
[ageKey]: 27,
[jobKey]: 'Software engineer'
};
console.log(person);
// 可以将对象的方法名简写
let person = {
sayName: function(name) {
console.log(`My name is ${name}`);
}
};
// 等价于
let person = {
sayName(name) {
console.log(`My name is ${name}`);
}
}
person.sayName('Matt'); // My name is Matt
// 简写方法名与可计算属性键相互兼容
const methodKey = 'sayName';
let person = {
[methodKey](name) {
console.log(`My name is ${name}`);
}
}
person.sayName('Matt'); // My name is Matt
对象解构
// 不使用对象解构的情况
let person = {
name: 'Matt',
age: 27
};
let personName = person.name,
personAge = person.age;
console.log(personName); // Matt
console.log(personAge); // 27
// 使用对象解构的情况
let { name: personName, age: personAge } = person;
console.log(personName); // Matt
console.log(personAge); // 27
// 对象解构配合简写语法
let { name, age } = person;
console.log(name); // Matt
console.log(age); // 27
// 如果引用的属性不存在,则该变量的值就是 undefined
let { name, job } = person;
console.log(name); // Matt
console.log(job); // undefined
// 可以在解构时定义默认值
let { name, job='Software engineer' } = person;
console.log(name); // Matt
console.log(job); // Software engineer
// null 和 undefined 不能被解构,否则会抛出错误
let { _ } = null; // TypeError
let { _ } = undefined; // TypeError
// 解构并不要求变量必须在解构表达式中声明
// 如果是给事先声明的变量赋值,则赋值表达式 必须包含在一对括号中
let personName, personAge;
({name: personName, age: personAge} = person);
console.log(personName, personAge); // Matt, 27
工厂模式
function createPerson(name, age, job) {
let o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function() {
console.log(this.name);
};
return o;
}
let person1 = createPerson("Nicholas", 29, "Software Engineer");
let person2 = createPerson("Greg", 27, "Doctor");
构造函数模式
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = function() {
console.log(this.name);
};
}
let person1 = new Person("Nicholas", 29, "Software Engineer");
let person2 = new Person("Greg", 27, "Doctor");
Person() 构造函数代替了 createPerson() 工厂函数,区别在于:
在实例化时,如果不想传参数,则构造函数后面的括号可加可不加
function Person() {
this.name = "Jake";
this.sayName = function() {
console.log(this.name);
};
}
let person1 = new Person();
let person2 = new Person;
构造函数与普通函数唯一的区别就是调用方式不同,任何函数只要使用 new 操作符调用就是构造函数,否则就是普通函数
// 作为构造函数
let person = new Person("Nicholas", 29, "Software Engineer");
person.sayName(); // "Nicholas"
// 作为函数调用
Person("Greg", 27, "Doctor"); // 对象被添加到了window上
window.sayName(); // "Greg"
// 在另一个对象的作用域中调用
let o = new Object();
Person.call(o, "Kristen", 25, "Nurse"); o.sayName(); // "Kristen"
通过构造函数创建的对象存在一个问题,即对象实例上的方法都是全新创建的
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = function() {
console.log(this.name);
};
}
let person1 = new Person("Nicholas", 29, "Software Engineer");
let person2 = new Person("Greg", 27, "Doctor");
console.log(person1.sayName === person2.sayName); // false
解决方式可以把方法定义在构造函数外部
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = sayName;
}
function sayName() {
console.log(this.name);
};
let person1 = new Person("Nicholas", 29, "Software Engineer");
let person2 = new Person("Greg", 27, "Doctor");
console.log(person1.sayName === person2.sayName); // true
但是这又引出了新的问题,如果这个对象需要多个方法,那么就要在全局作用域中定义多个函数。这会导致自定义类型引用的代码不能很好地聚集一起。因此又引出了第三种模式,原型模式
原型模式
function Person() {}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function() {
console.log(this.name);
};
let person1 = new Person();
let person2 = new Person();
person1.sayName(); // "Nicholas"
person2.sayName(); // "Nicholas"
console.log(person1.sayName === person2.sayName); // true
每个函数都会创建一个 prototype 属性,这个属性是一个对象,包含应该由特定引用类型的实例共享的属性和方法。使用原型对象的好处是,在它上面定义的属性和方法可以被对象实例共享。
在通过对象访问属性时,会按照这个属性的名称开始搜索。搜索开始于对象实例本身。如果在这个实例上发现了给定的名称,则返回该名称对应的值。如果没有找到这个属性,则搜索会沿着指针进入原型对象,然后在原型对象上找到属性后,再返回对应的值。
如果在实例上添加了一个与原型对象中同名的属性,那就会在实例上创建这个属性,这个属性会覆盖原型对象上的属性。
function Person() {}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function() {
console.log(this.name);
};
let person1 = new Person();
let person2 = new Person();
person2.name = "Greg";
person1.sayName(); // "Nicholas",来自原型
person2.sayName(); // "Greg",来自实例
delete person2.name; // 删除实例上的 name 属性
person2.sayName(); // "Nicholas",来自原型
实例通过 hasOwnProperty() 方法可以确定某个属性是在实例上还是在原型对象上
let person1 = new Person();
let person2 = new Person();
person2.name = "Greg";
console.log(person2.hasOwnProperty("name")); // true
person2.sayName(); // "Greg",来自实例
delete person2.name; // 删除实例上的 name 属性
console.log(person2.hasOwnProperty("name")); // false
person2.sayName(); // "Nicholas",来自原型
使用 in 操作符可以通过对象访问指定属性,无论该属性是在实例上还是在原型上。存在属性则返回 true,不存在则返回 false
let person = new Person();
person.sayName(); // "Nicholas"
console.log(person.hasOwnProperty("name")); // false
console.log("name" in person); // true
person.name = "Greg";
person.sayName(); // "Greg"
console.log(person.hasOwnProperty("name")); // true
console.log("name" in person); // true
原型模式之所以重要,不仅体现在自定义类型上,而且还因为它也是实现所有原生引用类型的模式。 所有原生引用类型的构造函数(包括 Object、Array、String 等)都在原型上定义了实例方法。比如,数组实例的 sort()方法就是 Array.prototype 上定义的,而字符串包装对象的 substring() 方法也是在 String.prototype 上定义的。
原型模式也不是没有问题,最明显的问题是所有实例默认都取得相同的属性值,但是原型的最主要问题源自它的共享特性。我们知道,原型上的所有属性是在实例间共享的,这对函数来说比较合适。另外包含原始值的属性也还好,可以通过在实例上添加同名属性来简单地遮蔽原型上的属性。真正的问题来自包含引用值的属性,例如:
function Person() {}
Person.prototype = {
constructor: Person,
name: "Nicholas",
age: 29,
job: "Software Engineer",
friends: ["Shelby", "Court"],
sayName() {
console.log(this.name);
}
};
let person1 = new Person();
let person2 = new Person();
person1.friends.push("Van");
console.log(person1.friends); // "Shelby,Court,Van"
console.log(person2.friends); // "Shelby,Court,Van"
console.log(person1.friends === person2.friends); // true
以上例子中,Person 原型中的 friends 属性维护着一个数组,向其中添加或者删除元素都会影响到所有通过 Person 原型创建的实例,因此在实际开发中通常不单独使用原型模式。